﻿namespace DCHealthCheck.DomainModel {

    #region Usings
    using DCHealthCheck.Common;
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Data;
    using System.Data.Entity;
    using System.Data.Entity.Infrastructure;
    using System.Data.Entity.Migrations;
    using System.Data.Entity.Validation;
    using System.Data.Objects;
    using System.Linq;
    using System.Threading;
    #endregion

    public class DCHealthCheckEntities : DbContext {

        #region Members
        internal List<string> ChangeTrackerDetails { get; set; }

        // The calling assembly must have initialized log4net for this to work
        private static readonly log4net.ILog DatabaseLog = log4net.LogManager.GetLogger("databaseLogger");

        /// <summary>
        /// Specifies if detailed information is logged about each entity when SaveChanges is called.
        /// </summary>
        private static readonly bool LogChangesDuringSave;

        /// <summary>
        /// Returns this as IObjectContextAdapter.ObjectContext, for the purpose of accessing the traditional ObjectContext
        /// </summary>
        internal System.Data.Objects.ObjectContext ObjectContext {
            get {
                var objectContextAdapter = this as IObjectContextAdapter;
                return objectContextAdapter.ObjectContext;
            }
        }

        /// <summary>
        /// Returns a list of all validation errors as DbEntityValidationResult objects
        /// </summary>
        internal List<DbEntityValidationResult> ValidationSummary { get; set; }

        #region DbSet properties
        public DbSet<Audit> AuditSet { get; set; }
        public DbSet<Certificate> CertificateSet { get; set; }
        public DbSet<DomainController> DomainControllerSet { get; set; }
        public DbSet<GroupPolicySysvolInfo> GroupPolicySysvolInfoSet { get; set; }
        public DbSet<HealthCheckSummary> HealthCheckSummarySet { get; set; }
        public DbSet<InstalledProduct> InstalledProductSet { get; set; }
        public DbSet<NetstatInfo> NetstatInfoSet { get; set; }
        public DbSet<PendingReplication> PendingReplicationSet { get; set; }
        public DbSet<ProcessInfo> ProcessInfoSet { get; set; }
        public DbSet<ReplicationNeighbor> ReplicationNeighborSet { get; set; }
        public DbSet<RoleFeature> RoleFeatureSet { get; set; }
        public DbSet<ServiceInfo> ServiceInfoSet { get; set; }
        public DbSet<SystemInfo> SystemInfoSet { get; set; }
        public DbSet<TagObject> TagObjectSet { get; set; }
        public DbSet<TagObjectValue> TagObjectValueSet { get; set; }
        public DbSet<TimeSyncInfo> TimeSyncInfoSet { get; set; }
        #endregion
        #endregion

        #region Constructors
        /// <summary>
        /// The constructors for the DbContext wrapper
        /// </summary>
        /// <remarks>
        /// Lazy Loading disabled:
        /// http://msdn.microsoft.com/en-us/library/vstudio/bb896304%28v=vs.100%29.aspx
        /// With binary serialization and data contract serialization, related objects are serialized together with the primary object. 
        /// XML serialization does not serialize related objects. When serializing entities, disable lazy loading. Lazy loading performs a query 
        /// for each relationship navigation property that is accessed, and both binary and WCF data contract serializers access all 
        /// relationship navigation properties. This can cause many unexpected queries to be performed during serialization. For more information, 
        /// see Serializing Objects: http://msdn.microsoft.com/en-us/library/vstudio/bb738446(v=vs.100).aspx
        /// </remarks>
        static DCHealthCheckEntities() {
            LogChangesDuringSave = ConfigurationManager.AppSettings["LogChangesDuringSave"] != null ? Convert.ToBoolean(ConfigurationManager.AppSettings["LogChangesDuringSave"]) : false;        
        }

        public DCHealthCheckEntities()
            : base("DCHealthCheckEntities") {
            this.Configuration.AutoDetectChangesEnabled = false;
            this.Configuration.LazyLoadingEnabled = false;
            this.Configuration.ProxyCreationEnabled = true;
            this.Configuration.ValidateOnSaveEnabled = true;

            this.ObjectContext.ObjectMaterialized += OnObjectMaterialized;
            this.ValidationSummary = new List<DbEntityValidationResult>();
            this.ChangeTrackerDetails = new List<string>();
        }
        #endregion

        #region Methods
        /// <summary>
        /// Builds the custom OriginalValues collection for the IObjectWithState objects
        /// </summary>
        /// <param name="originalValues">The original values collection of the materialized entity</param>
        /// <returns>A Dictionary of name value pairs of the OriginalValues</returns>
        /// <remarks>
        /// http://msdn.microsoft.com/en-us/library/vstudio/bb896304%28v=vs.100%29.aspx
        /// Stateless services are recommended. Services should be designed such that an object context is only maintained for the duration of a request 
        /// or a response. The message exchange pattern should include enough information so that changes can be applied without having to persist objects 
        /// or re-query the data source to retrieve the original object. For example, a service that allows a client to update objects should require that
        /// the updated object be returned along with the original object. This enables changes to be applied to the original object by the Web service 
        /// without having to retrieve the original object from the database or persist it in memory. For more information, see How to: Apply Changes Made 
        /// to a Detached Object: http://msdn.microsoft.com/en-us/library/vstudio/bb896248(v=vs.100).aspx
        /// </remarks>
        internal Dictionary<string, object> BuildOriginalValues(DbPropertyValues originalValues) {
            var result = new Dictionary<string, object>();
            foreach (var propertyName in originalValues.PropertyNames) {
                var value = originalValues[propertyName];
                if (value as DbPropertyValues != null) {
                    result[propertyName] = BuildOriginalValues(value as DbPropertyValues);
                }
                else {
                    result[propertyName] = value;
                }
            }
            return result;
        }

        /// <summary>
        /// Detaches all entities from the ObjectStateManager
        /// </summary>
        private void DetachObjectStateManagerEntries() {
            int detachedCount = 0;
            foreach (var entry in this.ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Deleted | EntityState.Modified | EntityState.Unchanged)) {
                this.ObjectContext.Detach(entry.Entity);
                detachedCount++;
            }

            if (detachedCount > 0) {
                DatabaseLog.DebugFormat("[ThreadId: {0}] {1} Detached: {2} entries from the ObjectStateManager.",
                        Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName(), detachedCount);
            }
        }

        protected void GetChangeTrackerEntriesDetail() {
            this.ChangeTrackerDetails.Clear();
            var changeTrackerEntries = from dbEntityEntry in this.ChangeTracker.Entries()
                                       where dbEntityEntry.State != EntityState.Unchanged
                                       select dbEntityEntry;

            foreach (var entry in changeTrackerEntries) {
                if (entry.Entity as Audit != null) continue;
                this.ChangeTrackerDetails.Add(string.Format("{0} entity type: {1}:", entry.State, entry.Entity.GetType()));

                switch (entry.State) {
                    case EntityState.Added:
                        LogPropertyValues(entry.CurrentValues, entry.CurrentValues.PropertyNames);
                        break;

                    case EntityState.Deleted:
                        LogPropertyValues(entry.OriginalValues, GetKeyPropertyNames(entry.Entity));
                        break;

                    case EntityState.Modified:
                        var modifiedPropertyNames =
                            from propertyName in entry.CurrentValues.PropertyNames
                            where entry.Property(propertyName).IsModified
                            select propertyName;

                        LogPropertyValues(entry.CurrentValues, GetKeyPropertyNames(entry.Entity).Concat(modifiedPropertyNames));
                        break;
                }
            }
        }

        /// <summary>
        /// Gets the key property names for the specified entity
        /// </summary>
        /// <param name="entity">The entity</param>
        /// <returns>A list of key property names</returns>
        private IEnumerable<string> GetKeyPropertyNames(object entity) {
            return this.ObjectContext
                .ObjectStateManager
                .GetObjectStateEntry(entity)
                .EntityKey
                .EntityKeyValues
                .Select(k => k.Key);
        }

        /// <summary>
        /// Constructs entity relationships
        /// </summary>
        /// <param name="modelBuilder">The modelBuilder</param>
        protected override void OnModelCreating(DbModelBuilder modelBuilder) {
            modelBuilder.Entity<SystemInfo>()
                .HasRequired(x => x.DomainController)
                .WithMany(x => x.SystemInfos)
                .HasForeignKey(x => x.DomainControllerId)
                .WillCascadeOnDelete(true);

            modelBuilder.Entity<SystemInfo>()
                .HasOptional(x => x.GroupPolicySysvolInfo)
                .WithRequired(x => x.SystemInfo)
                .WillCascadeOnDelete(true);

            modelBuilder.Entity<SystemInfo>()
                .HasOptional(x => x.TimeSyncInfo)
                .WithRequired(x => x.SystemInfo)
                .WillCascadeOnDelete(true);

            modelBuilder.Entity<NetstatInfo>()
                .HasOptional(x => x.ProcessInfo)
                .WithMany()
                .WillCascadeOnDelete(false);

            modelBuilder.Entity<TagObjectValue>()
                .HasRequired(x => x.TagObject)
                .WithMany(y => y.TagObjectValues)
                .HasForeignKey(x => x.TagObjectId)
                .WillCascadeOnDelete(false);
        }

        /// <summary>
        /// When an entity is materialized from the database, builds the collection of OriginalValues for concurrency purposes
        /// </summary>
        /// <param name="sender">The sending object</param>
        /// <param name="e">The ObjectMaterializedEventArgs event arguments</param>
        private void OnObjectMaterialized(object sender, ObjectMaterializedEventArgs e) {
            var entity = e.Entity as IObjectWithState;
            if (entity != null) {
                var changeTrackerEntry = this.Entry(entity);

                // .AsNoTracking() entities are detached/will not have OriginalValues
                if ((changeTrackerEntry != null) && (changeTrackerEntry.State != EntityState.Detached)) {
                    if (changeTrackerEntry.OriginalValues != null) {
                        if (changeTrackerEntry.State == EntityState.Modified) {
                            if (changeTrackerEntry.OriginalValues.PropertyNames.All(x => changeTrackerEntry.OriginalValues[x].Equals(changeTrackerEntry.CurrentValues[x]))) {
                                DatabaseLog.WarnFormat("[ThreadId: {0}] {1} Entity type: {2}; Id: {3}; Resetting state to Unchanged to due it materialized in the Modified state, but all Original and Current values are the same.",
                                    Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName(), entity.GetType(), entity.EntityKey);

                                changeTrackerEntry.State = EntityState.Unchanged;
                            }
                        }

                        // re-check
                        if (changeTrackerEntry.State == EntityState.Modified) {
                            throw new ApplicationException("Entity materialized in the Modified state.");
                        }

                        if ((changeTrackerEntry.State != EntityState.Added) && (changeTrackerEntry.State != EntityState.Deleted)) {
                            entity.DetachedState = DetachedState.Unchanged;
                            entity.StartingOriginalValues = BuildOriginalValues(changeTrackerEntry.OriginalValues);
                        }
                    }

                    // TODO: test
                    //this.ObjectContext.Detach(entity);
                }
            }
        }

        /// <summary>
        /// Logs a summary of database operations
        /// </summary>
        protected void LogChangeTrackerEntriesSummary() {
            int addedEntities = 0;
            int deletedEntities = 0;
            int modifiedEntities = 0;
            int totalEntitiesToSave = 0;

            if (this.ChangeTracker.Entries().Count() > 0) {
                addedEntities = this.ChangeTracker.Entries().Where(x => (x != null) && (x.State == EntityState.Added)).Count();
                deletedEntities = this.ChangeTracker.Entries().Where(x => (x != null) && (x.State == EntityState.Deleted)).Count();
                modifiedEntities = this.ChangeTracker.Entries().Where(x => (x != null) && (x.State == EntityState.Modified)).Count();
                totalEntitiesToSave = addedEntities + deletedEntities + modifiedEntities;
            }

            DatabaseLog.DebugFormat("[ThreadId: {0}] {1} Total number of entities to save: {2}; Added: {3}; Deleted: {4}; Modified: {5}",
                Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName(), totalEntitiesToSave, addedEntities, deletedEntities, modifiedEntities);
        }

        /// <summary>
        /// Logs the values of an entity's properties
        /// </summary>
        /// <param name="values">The list of DbPropertyValues</param>
        /// <param name="propertiesToPrint">The list of properties to log</param>
        /// <param name="indent">The indent level of the log</param>
        private void LogPropertyValues(DbPropertyValues values, IEnumerable<string> propertiesToPrint, int indent = 1) {
            foreach (var propertyName in propertiesToPrint) {
                var complexPropertyValues = values[propertyName] as DbPropertyValues;
                if (complexPropertyValues != null) {
                    this.ChangeTrackerDetails.Add(string.Format("{0}- Complex property: {1}:", string.Empty.PadLeft(indent), propertyName));
                    this.LogPropertyValues(complexPropertyValues, complexPropertyValues.PropertyNames, indent++);
                }
                else {
                    this.ChangeTrackerDetails.Add(string.Format("{0}- {1}: {2}", string.Empty.PadLeft(indent), propertyName, values[propertyName]));
                }
            }
        }

        /// <summary>
        /// Logs the contents of a list of validation errors
        /// </summary>
        /// <param name="results"></param>
        private void LogValidationErrors(IEnumerable<DbEntityValidationResult> results) {
            this.ValidationSummary = new List<DbEntityValidationResult>();
            int counter = 0;
            foreach (DbEntityValidationResult result in results) {
                this.ValidationSummary.Add(result);

                counter++;

                DatabaseLog.DebugFormat("[ThreadId: {0}] {1} Validation error entry#: {2}; Entity Type: {3}; error count: {4}",
                    Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName(),
                    counter, result.Entry.Entity.GetType().Name, result.ValidationErrors.Count);

                foreach (DbValidationError error in result.ValidationErrors) {
                    string message = string.Format("[ThreadId: {0}] {1}  - Property Name: {2}; Error Message: {3}",
                        Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName(), error.PropertyName, error.ErrorMessage);
                    Console.WriteLine(message);
                    DatabaseLog.DebugFormat(message);
                }
            }
        }

        /// <summary>
        /// Saves all entities in the current object graph
        /// </summary>
        /// <returns>The number of saved entities</returns>
        public override int SaveChanges() {
            DatabaseLog.DebugFormat("[ThreadId: {0}] {1} ", Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName());

            this.LogValidationErrors(GetValidationErrors());
            this.LogChangeTrackerEntriesSummary();

            if (LogChangesDuringSave) {
                if (this.ChangeTrackerDetails.Count == 0) {
                    this.GetChangeTrackerEntriesDetail();
                }

                if (this.ChangeTrackerDetails.Count > 0) {
                    DatabaseLog.DebugFormat("[ThreadId: {0}] {1} change details:", Thread.CurrentThread.ManagedThreadId, Extensions.CurrentMethodName());
                    DatabaseLog.Debug(this.ChangeTrackerDetails.ToFormattedString());
                }
            }

            this.ChangeTrackerDetails.Clear();

            int savedEntityCount = base.SaveChanges();
            return savedEntityCount;
        }

        #endregion
    }
}
